/* Copyright (c) 2010, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */

package com.cburch.logisim.circuit;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import com.cburch.logisim.comp.Component;
import com.cburch.logisim.data.Location;

class WireRepair extends CircuitTransaction {

    private static class MergeSets {
        private final HashMap<Wire,ArrayList<Wire>> map;

        MergeSets() {
            map = new HashMap<Wire,ArrayList<Wire>>();
        }

        void merge(Wire a, Wire b) {
            ArrayList<Wire> set0 = map.get(a);
            ArrayList<Wire> set1 = map.get(b);
            if (set0 == null && set1 == null) {
                set0 = new ArrayList<Wire>(2);
                set0.add(a);
                set0.add(b);
                map.put(a, set0);
                map.put(b, set0);
            } else if (set0 == null && set1 != null) {
                set1.add(a);
                map.put(a, set1);
            } else if (set0 != null && set1 == null) {
                set0.add(b);
                map.put(b, set0);
            // neither is null, and they are different
            } else if (set0 != set1) {
                // ensure set1 is the larger
                if (set0.size() > set1.size()) {
                    ArrayList<Wire> temp = set0;
                    set0 = set1;
                    set1 = temp;
                }
                set1.addAll(set0);
                for (Wire w : set0) {
                    map.put(w, set1);
                }
            }
        }

        Collection<ArrayList<Wire>> getMergeSets() {
            IdentityHashMap<ArrayList<Wire>,Boolean> lists;
            lists = new IdentityHashMap<ArrayList<Wire>,Boolean>();
            for (ArrayList<Wire> list : map.values()) {
                lists.put(list, Boolean.TRUE);
            }
            return lists.keySet();
        }
    }

    private Circuit circuit;

    public WireRepair(Circuit circuit) {
        this.circuit = circuit;
    }

    @Override
    protected Map<Circuit,Integer> getAccessedCircuits() {
        return Collections.singletonMap(circuit, READ_WRITE);
    }

    @Override
    protected void run(CircuitMutator mutator) {
        doMerges(mutator);
        doOverlaps(mutator);
        doSplits(mutator);
    }

    /* for debugging:
    private void printWires(String prefix, PrintStream out) {
        boolean first = true;
        for (Wire w : circuit.getWires()) {
            if (first) {
                out.println(prefix + ": " + w);
                first = false;
            } else {
                out.println("      " + w);
            }
        }
        out.println(prefix + ": none");
    }
    */

    private void doMerges(CircuitMutator mutator) {
        MergeSets sets = new MergeSets();
        for (Location loc : circuit.wires.points.getSplitLocations()) {
            Collection<?> at = circuit.getComponents(loc);
            if (at.size() == 2) {
                Iterator<?> atit = at.iterator();
                Object at0 = atit.next();
                Object at1 = atit.next();
                if (at0 instanceof Wire && at1 instanceof Wire) {
                    Wire w0 = (Wire) at0;
                    Wire w1 = (Wire) at1;
                    if (w0.isParallel(w1)) {
                        sets.merge(w0, w1);
                    }
                }
            }
        }

        ReplacementMap repl = new ReplacementMap();
        for (ArrayList<Wire> mergeSet : sets.getMergeSets()) {
            if (mergeSet.size() > 1) {
                ArrayList<Location> locs = new ArrayList<Location>(2 * mergeSet.size());
                for (Wire w : mergeSet) {
                    locs.add(w.getEnd0());
                    locs.add(w.getEnd1());
                }
                Collections.sort(locs);
                Location e0 = locs.get(0);
                Location e1 = locs.get(locs.size() - 1);
                Wire wnew = Wire.create(e0, e1);
                Collection<Wire> wset = Collections.singleton(wnew);

                for (Wire w : mergeSet) {
                    if (!w.equals(wset)) {
                        repl.put(w, wset);
                    }
                }
            }
        }
        mutator.replace(circuit, repl);
    }

    private void doOverlaps(CircuitMutator mutator) {
        HashMap<Location,ArrayList<Wire>> wirePoints;
        wirePoints = new HashMap<Location,ArrayList<Wire>>();
        for (Wire w : circuit.getWires()) {
            for (Location loc : w) {
                ArrayList<Wire> locWires = wirePoints.get(loc);
                if (locWires == null) {
                    locWires = new ArrayList<Wire>(3);
                    wirePoints.put(loc, locWires);
                }
                locWires.add(w);
            }
        }

        MergeSets mergeSets = new MergeSets();
        for (ArrayList<Wire> locWires : wirePoints.values()) {
            if (locWires.size() > 1) {
                for (int i = 0, n = locWires.size(); i < n; i++) {
                    Wire w0 = locWires.get(i);
                    for (int j = i + 1; j < n; j++) {
                        Wire w1 = locWires.get(j);
                        if (w0.overlaps(w1, false)) {
                            mergeSets.merge(w0, w1);
                        }
                    }
                }
            }
        }

        ReplacementMap replacements = new ReplacementMap();
        Set<Location> splitLocs = circuit.wires.points.getSplitLocations();
        for (ArrayList<Wire> mergeSet : mergeSets.getMergeSets()) {
            if (mergeSet.size() > 1) {
                doMergeSet(mergeSet, replacements, splitLocs);
            }
        }
        mutator.replace(circuit, replacements);
    }

    private void doMergeSet(ArrayList<Wire> mergeSet, ReplacementMap replacements,
            Set<Location> splitLocs) {
        TreeSet<Location> ends = new TreeSet<Location>();
        for (Wire w : mergeSet) {
            ends.add(w.getEnd0());
            ends.add(w.getEnd1());
        }
        Wire whole = Wire.create(ends.first(), ends.last());

        TreeSet<Location> mids = new TreeSet<Location>();
        mids.add(whole.getEnd0());
        mids.add(whole.getEnd1());
        for (Location loc : whole) {
            if (splitLocs.contains(loc)) {
                for (Component comp : circuit.getComponents(loc)) {
                    if (!mergeSet.contains(comp)) {
                        mids.add(loc);
                        break;
                    }
                }
            }
        }

        ArrayList<Wire> mergeResult = new ArrayList<Wire>();
        if (mids.size() == 2) {
            mergeResult.add(whole);
        } else {
            Location e0 = mids.first();
            for (Location e1 : mids) {
                mergeResult.add(Wire.create(e0, e1));
                e0 = e1;
            }
        }

        for (Wire w : mergeSet) {
            ArrayList<Component> wRepl = new ArrayList<Component>(2);
            for (Wire w2 : mergeResult) {
                if (w2.overlaps(w, false)) {
                    wRepl.add(w2);
                }
            }
            replacements.put(w, wRepl);
        }
    }

    private void doSplits(CircuitMutator mutator) {
        Set<Location> splitLocs = circuit.wires.points.getSplitLocations();
        ReplacementMap repl = new ReplacementMap();
        for (Wire w : circuit.getWires()) {
            Location w0 = w.getEnd0();
            Location w1 = w.getEnd1();
            ArrayList<Location> splits = null;
            for (Location loc : splitLocs) {
                if (w.contains(loc) && !loc.equals(w0) && !loc.equals(w1)) {
                    if (splits == null) {
                        splits = new ArrayList<Location>();
                    }

                    splits.add(loc);
                }
            }
            if (splits != null) {
                splits.add(w1);
                Collections.sort(splits);
                Location e0 = w0;
                ArrayList<Wire> subs = new ArrayList<Wire>(splits.size());
                for (Location e1 : splits) {
                    subs.add(Wire.create(e0, e1));
                    e0 = e1;
                }
                repl.put(w, subs);
            }
        }
        mutator.replace(circuit, repl);
    }
}
